package sk.stuba.fiit.ltasys.web.mvc.model.service.core;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.bouncycastle.jce.provider.JCEIESCipher.ECIES;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampResponse;
import org.etsi.uri._01903.v1_3.CRLValuesType;
import org.etsi.uri._01903.v1_3.CertificateValuesType;
import org.etsi.uri._01903.v1_3.DigestAlgAndValueType;
import org.etsi.uri._01903.v1_3.EncapsulatedPKIDataType;
import org.etsi.uri._01903.v1_3.RevocationValuesType;
import org.etsi.uri._01903.v1_3.XAdESTimeStampType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.stereotype.Service;
import org.w3._2000._09.xmldsig.CanonicalizationMethodType;
import org.w3._2000._09.xmldsig.DigestMethodType;
import org.w3._2000._09.xmldsig.KeyInfoType;
import org.w3._2000._09.xmldsig.TransformType;
import org.w3._2000._09.xmldsig.TransformsType;
import org.w3._2000._09.xmldsig.X509DataType;

import com.sun.org.apache.xerces.internal.jaxp.datatype.XMLGregorianCalendarImpl;

import sk.stuba.fiit.ltasys.web.mvc.model.entity.ArchivePackageEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.BinaryDataEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.CertificateEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.CrlEntity;
import sk.stuba.fiit.ltasys.web.mvc.model.entity.EvidenceRecordEntity;
import sk.stuba.fiit.ltasys.ws.schema.beans.ArchivePackageType;
import sk.stuba.fiit.ltasys.ws.schema.beans.ArchiveResponse;
import sk.stuba.fiit.ltasys.ws.schema.beans.DataRefType;
import sk.stuba.fiit.ltasys.ws.schema.beans.DataType;
import sk.stuba.fiit.ltasys.ws.schema.beans.EvidenceRecordSequenceType;
import sk.stuba.fiit.ltasys.ws.schema.beans.EvidenceRecordType;
import sk.stuba.fiit.ltasys.ws.schema.beans.UriRefType;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;


@Service("packageCreator")
public class PackageForUserCreator {


	@Autowired 
	private Jaxb2Marshaller marshallerService;
	
	public ArchiveResponse assembleArchivePackage(
			ArchivePackageEntity archivPackage) throws Exception {
		ArchiveResponse response = new ArchiveResponse();

		DataType datatype = new DataType();
		datatype.setId("Id" + System.currentTimeMillis());
		datatype.setBinaryData(getUserDataFromArchivePackage(archivPackage));
		datatype.setToken(archivPackage.getToken());
		response.setData(datatype);

		ArchivePackageType archiveType = new ArchivePackageType();
		archiveType.setToken(archivPackage.getToken());
		EvidenceRecordSequenceType erseq = new EvidenceRecordSequenceType();

		List<EvidenceRecordType> ert = new ArrayList<EvidenceRecordType>();
		List<EvidenceRecordEntity> record = new ArrayList<EvidenceRecordEntity>(
				archivPackage.getArchiveRecord());
		Collections.sort(record);

		for (int i = 0; i < record.size(); i++) {
			EvidenceRecordType evidenceRecord = new EvidenceRecordType();
			evidenceRecord.setId("Id" + System.currentTimeMillis());

			GregorianCalendar cal = new GregorianCalendar();
			cal.setTime(record.get(i).getCreated());
			XMLGregorianCalendarImpl calendar = new XMLGregorianCalendarImpl(
					cal);
			evidenceRecord.setCreationDate(calendar);
			evidenceRecord.setCertificateValues(processCertificateValue(record
					.get(i)));
			evidenceRecord.setRevocationValues(processRevocateValue(record
					.get(i)));
			evidenceRecord.setProtectionTimestamp(processTimestampValue(record
					.get(i)));
			evidenceRecord
					.setProtectionDigestTimestamp(processTimeStampValueDigest(record
							.get(i)));
			if (i == 0) {
				evidenceRecord
						.setDataRef(processFirstDataReference(archivPackage));
			} else {
				evidenceRecord.setDataRef(processDataReference(archivPackage,record.get(i - 1),evidenceRecord.getId()));
			}
			ert.add(evidenceRecord);
		}

		erseq.withEvidenceRecord(ert);
		archiveType.setEvidenceRecordSequence(erseq);
		
		StringWriter writer = new StringWriter();
		Result result = new StreamResult(writer);
		marshallerService.marshal(archiveType, result);
		byte [] data = result.toString().getBytes();
		
		BASE64Encoder decoder = new BASE64Encoder();
	
		response.setArchivepackage(decoder.encode(writer.toString().getBytes()));
		return response;
	}

	private DataRefType processDataReference(
			ArchivePackageEntity prchivPackage, EvidenceRecordEntity er, String string) throws Exception {
		DataRefType result = new DataRefType();
		List<UriRefType> uris = new ArrayList<UriRefType>();	
		
		TransformsType tst = new TransformsType();
		TransformType tt = new TransformType();
		CanonicalizationMethodType canMethod = new CanonicalizationMethodType();
		canMethod.setAlgorithm(canonicalMethod);
		tt.withContent(canMethod);
		tst.withTransform(tt);
		
		UriRefType uriRefs = new UriRefType();
		uriRefs.setURI("Target=#"+string);
		uriRefs.withTransforms(tst);
		uris.add(uriRefs);
		

		for (CertificateEntity cert : er.getCertificates()) {
			uriRefs = new UriRefType();
			uriRefs.setURI("Target=#cert"+cert.getSerialNumber());
			uriRefs.withTransforms(tst);
			uris.add(uriRefs);
		}
		
		for (CrlEntity crls : er.getCrls()){
			uriRefs = new UriRefType();
			uriRefs.setURI("Target=#crl"+crls.getCrl_id());
			uriRefs.withTransforms(tst);
			uris.add(uriRefs);
		}
		
		DigestAlgAndValueType protectionDigest = new DigestAlgAndValueType();
		DigestMethodType digMethod = new DigestMethodType();
		digMethod.setAlgorithm(digestMethod);
		protectionDigest.setDigestMethod(digMethod );
		protectionDigest.setDigestValue(comuteHashRoot(prchivPackage,er));
		result.withProtectionDigestDataRef(protectionDigest );
		result.withURI(uris);
		return result;
	}

	private DataRefType processFirstDataReference(
			ArchivePackageEntity archivPackage) throws Exception {
		DataRefType result = new DataRefType();

		List<byte[]> unorderedByteList = new ArrayList<byte[]>();

		List<UriRefType> uris = new ArrayList<UriRefType>();
		for (BinaryDataEntity bde : archivPackage.getUserData().getData()) {
			UriRefType uriRefs = new UriRefType();
			uriRefs.setURI(bde.getName());
			TransformsType tst = new TransformsType();
			TransformType tt = new TransformType();
			CanonicalizationMethodType canMethod = new CanonicalizationMethodType();
			canMethod.setAlgorithm(canonicalMethod);
			tt.withContent(canMethod);
			tst.withTransform(tt);
			uriRefs.withTransforms(tst);
			uris.add(uriRefs);
			unorderedByteList.add(bde.getData());
		}
		DigestAlgAndValueType protectionDigest = new DigestAlgAndValueType();
		protectionDigest.setDigestValue(computeNodeHash(unorderedByteList));
		DigestMethodType digMeth = new DigestMethodType();
		digMeth.setAlgorithm(digestMethod);
		protectionDigest.setDigestMethod(digMeth);

		result.withProtectionDigestDataRef(protectionDigest);
		result.withURI(uris);
		return result;
	}

	private DigestAlgAndValueType processTimeStampValueDigest(
			EvidenceRecordEntity evidenceRecordEntity) throws Exception {
		TimeStampResponse tsr = new TimeStampResponse(evidenceRecordEntity
				.getTimestamp().getTimestamp());
		DigestAlgAndValueType davt = new DigestAlgAndValueType();
		DigestMethodType digMethod = new DigestMethodType();
		digMethod.setAlgorithm(digestMethod);
		davt.setDigestMethod(digMethod);
		davt.setDigestValue(tsr.getTimeStampToken().getTimeStampInfo()
				.getMessageImprintDigest());
		return davt;
	}

	private XAdESTimeStampType processTimestampValue(
			EvidenceRecordEntity evidenceRecordEntity) {

		XAdESTimeStampType timestamp = new XAdESTimeStampType();
		CanonicalizationMethodType canMethod = new CanonicalizationMethodType();
		canMethod.setAlgorithm(canonicalMethod);
		timestamp.setCanonicalizationMethod(canMethod);
		EncapsulatedPKIDataType encTimeStamp = new EncapsulatedPKIDataType();
		encTimeStamp.setValue(evidenceRecordEntity.getTimestamp()
				.getTimestamp());
		timestamp.withEncapsulatedTimeStampOrXMLTimeStamp(encTimeStamp);
		return timestamp;
	}

	public static String canonicalMethod = "http://www.w3.org/2006/12/xml-c14n11";
	public static String digestMethod = "http://www.w3.org/2000/09/xmldsig#SHA-1";

	private RevocationValuesType processRevocateValue(
			EvidenceRecordEntity evidenceRecordEntity) {
		RevocationValuesType rvt = new RevocationValuesType();
		CRLValuesType crlValue = new CRLValuesType();
		EncapsulatedPKIDataType encapsulatedType = new EncapsulatedPKIDataType();
		encapsulatedType.setValue(evidenceRecordEntity.getCrls().iterator()
				.next().getCrl());
		encapsulatedType.setId("Idcrl"+evidenceRecordEntity.getCrls().iterator().next().getCrl_id());
		crlValue.withEncapsulatedCRLValue(encapsulatedType);
		rvt.withCRLValues(crlValue);
		return rvt;
	}

	private CertificateValuesType processCertificateValue(
			EvidenceRecordEntity evidenceRecordEntity) {

		CertificateValuesType cvt = new CertificateValuesType();

		for (CertificateEntity certEntity : evidenceRecordEntity
				.getCertificates()) {
			EncapsulatedPKIDataType encCert = new EncapsulatedPKIDataType();
			encCert.setValue(certEntity.getCert());
			encCert.setId("Idcert"+certEntity.getSerialNumber());
			cvt.withEncapsulatedX509CertificateOrOtherCertificate(encCert);
		}

		return cvt;
	}

	private byte[] computeNodeHash(List<byte[]> unorderedByteList)
			throws Exception {
		TreeSet<String> orderedData = new TreeSet<String>();
		MessageDigest hash = MessageDigest.getInstance("SHA", "BC");

		for (byte[] data : unorderedByteList) {
			hash.reset();
			hash.update(data);
			orderedData.add(new String(hash.digest()));
		}
		hash.reset();
		for (String hashData : orderedData) {
			hash.update(hashData.getBytes());
		}
		byte[] result = hash.digest();
		return result;
	}
	
	private byte [] comuteHashRoot(ArchivePackageEntity archiv, EvidenceRecordEntity evidence) throws Exception{
		List<EvidenceRecordEntity> list = new ArrayList<EvidenceRecordEntity>(archiv.getArchiveRecord());
		Collections.sort(list);
		
		//najstarsi zaznam
		if(list.get(0).equals(evidence) ){
			 List<byte[]> referenceData = new ArrayList<byte[]>();
			 //pridam obsah podpisu a obsah dat
			// referenceData.add(archiv.getUserData().getSignature().getBytes());
			 for(int i= 0; i< archiv.getUserData().getData().size(); i++){
				 referenceData.add(archiv.getUserData().getData().get(i).getData());
			 }
			 byte[] hashReference = computeNodeHash(referenceData); 
			 
			 //pridam aktualny certifikat
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (CertificateEntity certificate : evidence.getCertificates()) {
				 protectionData.add(certificate.getCert());
			 }
			 for (CrlEntity crl: evidence.getCrls()){
				 protectionData.add(crl.getCrl());
			 }			 
			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);			 
			 List<byte[]> timestampData = new ArrayList<byte[]>();
				timestampData.add(hashReference);
				timestampData.add(hashProtectionData);
				
			return computeNodeHash(timestampData);
		}else{
			byte[] hashReference = comuteHashRoot(archiv, list.get(list.indexOf(evidence)-1));
			 
			 List<byte[]> protectionData = new ArrayList<byte[]>();
			 for (CertificateEntity certificate : evidence.getCertificates()) {
				 protectionData.add(certificate.getCert());
			 }
			 for (CrlEntity crl: evidence.getCrls()){
				 protectionData.add(crl.getCrl());
			 }		

			 protectionData.add(list.get(list.indexOf(evidence)-1).getTimestamp().getTimestamp());
			 
			 byte[] hashProtectionData = computeNodeHash(protectionData);
			
			 List<byte[]> timestampData = new ArrayList<byte[]>();
			 timestampData.add(hashReference);
			 timestampData.add(hashProtectionData);	
			 return computeNodeHash(timestampData);
		}
	}

	private String getUserDataFromArchivePackage(
			ArchivePackageEntity archivPackage) throws Exception {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ZipOutputStream out = new ZipOutputStream(baos);

		for (int i = 0; i < archivPackage.getUserData().getData().size(); i++) {
			out.putNextEntry(new ZipEntry(archivPackage.getUserData().getData()
					.get(i).getName()));
			out.write(archivPackage.getUserData().getData().get(i).getData());
			out.closeEntry();
		}

		out.close();
		BASE64Encoder encoder = new BASE64Encoder();
		return encoder.encode(baos.toByteArray());
	}
}
